/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.dex.file;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.TreeMap;

import com.android.dx.util.AnnotatedOutput;
import com.android.dx.util.ExceptionWithContext;
import com.android.dx.util.Hex;

/**
 * A section of a {@code .dex} file which consists of a sequence of
 * {@link OffsettedItem} objects, which may each be of a different concrete
 * class and/or size. <b>Note:</b> It is invalid for an item in an instance of
 * this class to have a larger alignment requirement than the alignment of this
 * instance.
 */
public final class MixedItemSection extends Section {

	static enum SortType {
		/** no sorting */
		NONE,

		/** sort by type only */
		TYPE,

		/** sort in class-major order, with instances sorted per-class */
		INSTANCE;
	};

	/** {@code non-null;} sorter which sorts instances by type */
	private static final Comparator<OffsettedItem> TYPE_SORTER = new Comparator<OffsettedItem>() {

		public int compare(OffsettedItem item1, OffsettedItem item2) {
			ItemType type1 = item1.itemType();
			ItemType type2 = item2.itemType();
			return type1.compareTo(type2);
		}
	};

	/** {@code non-null;} the items in this part */
	private final ArrayList<OffsettedItem> items;

	/** {@code non-null;} items that have been explicitly interned */
	private final HashMap<OffsettedItem, OffsettedItem> interns;

	/** {@code non-null;} how to sort the items */
	private final SortType sort;

	/**
	 * {@code >= -1;} the current size of this part, in bytes, or {@code -1} if
	 * not yet calculated
	 */
	private int writeSize;

	/**
	 * Constructs an instance. The file offset is initially unknown.
	 * 
	 * @param name
	 *            {@code null-ok;} the name of this instance, for annotation
	 *            purposes
	 * @param file
	 *            {@code non-null;} file that this instance is part of
	 * @param alignment
	 *            {@code > 0;} alignment requirement for the final output; must
	 *            be a power of 2
	 * @param sort
	 *            how the items should be sorted in the final output
	 */
	public MixedItemSection(String name, DexFile file, int alignment,
			SortType sort) {
		super(name, file, alignment);

		this.items = new ArrayList<OffsettedItem>(100);
		this.interns = new HashMap<OffsettedItem, OffsettedItem>(100);
		this.sort = sort;
		this.writeSize = -1;
	}

	/** {@inheritDoc} */
	@Override
	public Collection<? extends Item> items() {
		return items;
	}

	/** {@inheritDoc} */
	@Override
	public int writeSize() {
		throwIfNotPrepared();
		return writeSize;
	}

	/** {@inheritDoc} */
	@Override
	public int getAbsoluteItemOffset(Item item) {
		OffsettedItem oi = (OffsettedItem) item;
		return oi.getAbsoluteOffset();
	}

	/**
	 * Gets the size of this instance, in items.
	 * 
	 * @return {@code >= 0;} the size
	 */
	public int size() {
		return items.size();
	}

	/**
	 * Writes the portion of the file header that refers to this instance.
	 * 
	 * @param out
	 *            {@code non-null;} where to write
	 */
	public void writeHeaderPart(AnnotatedOutput out) {
		throwIfNotPrepared();

		if (writeSize == -1) {
			throw new RuntimeException("write size not yet set");
		}

		int sz = writeSize;
		int offset = (sz == 0) ? 0 : getFileOffset();
		String name = getName();

		if (name == null) {
			name = "<unnamed>";
		}

		int spaceCount = 15 - name.length();
		char[] spaceArr = new char[spaceCount];
		Arrays.fill(spaceArr, ' ');
		String spaces = new String(spaceArr);

		if (out.annotates()) {
			out.annotate(4, name + "_size:" + spaces + Hex.u4(sz));
			out.annotate(4, name + "_off: " + spaces + Hex.u4(offset));
		}

		out.writeInt(sz);
		out.writeInt(offset);
	}

	/**
	 * Adds an item to this instance. This will in turn tell the given item that
	 * it has been added to this instance. It is invalid to add the same item to
	 * more than one instance, nor to add the same items multiple times to a
	 * single instance.
	 * 
	 * @param item
	 *            {@code non-null;} the item to add
	 */
	public void add(OffsettedItem item) {
		throwIfPrepared();

		try {
			if (item.getAlignment() > getAlignment()) {
				throw new IllegalArgumentException(
						"incompatible item alignment");
			}
		} catch (NullPointerException ex) {
			// Elucidate the exception.
			throw new NullPointerException("item == null");
		}

		items.add(item);
	}

	/**
	 * Interns an item in this instance, returning the interned instance (which
	 * may not be the one passed in). This will add the item if no equal item
	 * has been added.
	 * 
	 * @param item
	 *            {@code non-null;} the item to intern
	 * @return {@code non-null;} the equivalent interned instance
	 */
	public <T extends OffsettedItem> T intern(T item) {
		throwIfPrepared();

		OffsettedItem result = interns.get(item);

		if (result != null) {
			return (T) result;
		}

		add(item);
		interns.put(item, item);
		return item;
	}

	/**
	 * Gets an item which was previously interned.
	 * 
	 * @param item
	 *            {@code non-null;} the item to look for
	 * @return {@code non-null;} the equivalent already-interned instance
	 */
	public <T extends OffsettedItem> T get(T item) {
		throwIfNotPrepared();

		OffsettedItem result = interns.get(item);

		if (result != null) {
			return (T) result;
		}

		throw new NoSuchElementException(item.toString());
	}

	/**
	 * Writes an index of contents of the items in this instance of the given
	 * type. If there are none, this writes nothing. If there are any, then the
	 * index is preceded by the given intro string.
	 * 
	 * @param out
	 *            {@code non-null;} where to write to
	 * @param itemType
	 *            {@code non-null;} the item type of interest
	 * @param intro
	 *            {@code non-null;} the introductory string for non-empty
	 *            indices
	 */
	public void writeIndexAnnotation(AnnotatedOutput out, ItemType itemType,
			String intro) {
		throwIfNotPrepared();

		TreeMap<String, OffsettedItem> index = new TreeMap<String, OffsettedItem>();

		for (OffsettedItem item : items) {
			if (item.itemType() == itemType) {
				String label = item.toHuman();
				index.put(label, item);
			}
		}

		if (index.size() == 0) {
			return;
		}

		out.annotate(0, intro);

		for (Map.Entry<String, OffsettedItem> entry : index.entrySet()) {
			String label = entry.getKey();
			OffsettedItem item = entry.getValue();
			out.annotate(0, item.offsetString() + ' ' + label + '\n');
		}
	}

	/** {@inheritDoc} */
	@Override
	protected void prepare0() {
		DexFile file = getFile();

		/*
		 * It's okay for new items to be added as a result of an addContents()
		 * call; we just have to deal with the possibility.
		 */

		int i = 0;
		for (;;) {
			int sz = items.size();
			if (i >= sz) {
				break;
			}

			for (/* i */; i < sz; i++) {
				OffsettedItem one = items.get(i);
				one.addContents(file);
			}
		}
	}

	/**
	 * Places all the items in this instance at particular offsets. This will
	 * call {@link OffsettedItem#place} on each item. If an item does not know
	 * its write size before the call to {@code place}, it is that call which is
	 * responsible for setting the write size. This method may only be called
	 * once per instance; subsequent calls will throw an exception.
	 */
	public void placeItems() {
		throwIfNotPrepared();

		switch (sort) {
		case INSTANCE: {
			Collections.sort(items);
			break;
		}
		case TYPE: {
			Collections.sort(items, TYPE_SORTER);
			break;
		}
		}

		int sz = items.size();
		int outAt = 0;
		for (int i = 0; i < sz; i++) {
			OffsettedItem one = items.get(i);
			try {
				int placedAt = one.place(this, outAt);

				if (placedAt < outAt) {
					throw new RuntimeException("bogus place() result for "
							+ one);
				}

				outAt = placedAt + one.writeSize();
			} catch (RuntimeException ex) {
				throw ExceptionWithContext.withContext(ex, "...while placing "
						+ one);
			}
		}

		writeSize = outAt;
	}

	/** {@inheritDoc} */
	@Override
	protected void writeTo0(AnnotatedOutput out) {
		boolean annotates = out.annotates();
		boolean first = true;
		DexFile file = getFile();
		int at = 0;

		for (OffsettedItem one : items) {
			if (annotates) {
				if (first) {
					first = false;
				} else {
					out.annotate(0, "\n");
				}
			}

			int alignMask = one.getAlignment() - 1;
			int writeAt = (at + alignMask) & ~alignMask;

			if (at != writeAt) {
				out.writeZeroes(writeAt - at);
				at = writeAt;
			}

			one.writeTo(file, out);
			at += one.writeSize();
		}

		if (at != writeSize) {
			throw new RuntimeException("output size mismatch");
		}
	}
}
